feat: polyrepo workspace mode — repo-scoped lanes, merge, resume, naming, and dashboard - #21
Merged
Conversation
…ed modules Tests were reading from the old monolith task-orchestrator.ts but functions were refactored into taskplane/ modules (formatting.ts, execution.ts, worktree.ts, messages.ts, etc). Updated source resolution to read from all modules. Added vitest dual-mode wrapper to orch-direct-implementation. Updated isOrchestratedMode tests to match simplified implementation.
Step 0: Refactor lane allocation model Type changes: - Add repoId?: string to LaneAssignment, AllocatedLane, PersistedLaneRecord New functions in waves.ts: - groupTasksByRepo(): deterministic grouping by resolvedRepoId - generateLaneId(): repo-aware lane ID (lane-N or repoId/lane-N) - generateTmuxSessionName(): repo-aware session names Updated functions: - assignTasksToLanes(): optional laneOffset + repoId params - allocateLanes(): groups by repo, allocates per group, globally unique lane numbers, repo-aware laneId/tmuxSessionName Downstream propagation: - persistence.ts: persist repoId on lane records - resume.ts: restore repoId when reconstructing AllocatedLane Compatibility: - Repo mode (no resolvedRepoId): all tasks in single group, identical output to pre-TP-004 behavior - Lane numbers globally unique (engine.ts/resume.ts assumptions preserved) - laneNumber-keyed lookups in engine.ts/formatting.ts unchanged
- Add resolveRepoRoot() helper: resolves repoId → absolute repo root path from workspace config, falls back to default repoRoot in repo mode - Add resolveBaseBranch() helper: fallback chain (per-repo defaultBranch → detected current branch → batch baseBranch) - Add workspaceConfig parameter to allocateLanes() - Refactor Stage 3: loop over repo groups, call ensureLaneWorktrees() per group with group-specific repoRoot and baseBranch - Add cross-repo rollback: on failure in repo group N, roll back all previously-created worktrees from groups 1..N-1 - Update Stage 4: build worktree lookup from per-repo results - Remove duplicate function definitions from prior iteration - Add 19 unit tests for new helpers (resolveRepoRoot, resolveBaseBranch, groupTasksByRepo, generateLaneId, generateTmuxSessionName) - Zero new test failures (4 pre-existing failures unchanged)
…fix abort for workspace-mode sessions
- Add workspaceConfig parameter to executeWave() and pass to allocateLanes()
- Thread workspaceConfig from executeOrchBatch() and resumeOrchBatch()
- Fix selectAbortTargetSessions() to match workspace-mode session names
(<prefix>-<repoId>-lane-<N>) in addition to repo-mode (<prefix>-lane-<N>)
- Source laneId from PersistedLaneRecord instead of reconstructing as
lane-${laneNumber}, preserving repo dimension in workspace mode
- Verify cleanup is already repo-agnostic (listWorktrees uses prefix matching)
- Add 7 unit tests for workspace-mode abort behavior
…ycle - Add section 9 to waves-lanes-and-worktrees.md covering workspace mode: repo grouping, lane identity format, per-repo worktree provisioning, cross-repo rollback, and abort compatibility - Create .pi/local/docs/taskplane/polyrepo-support-spec.md (internal, gitignored) as comprehensive reference for finalized lane identity contract and repo-scoped worktree rules - Verify messages.ts unaffected: uses numeric laneNumber (globally unique), not string laneId
- Add WorkspaceRoutingConfig.strict boolean field (default: false) - Add TASK_ROUTING_STRICT fatal discovery error code - Parse routing.strict from taskplane-workspace.yaml - Enforce strict mode in resolveTaskRouting(): require explicit promptRepoId when strict is enabled, with actionable remediation guidance pointing users to ## Execution Target section - Repo mode and permissive workspace mode behavior unchanged
- 19 new tests covering strict routing enforcement in discovery - Tests: strict mode rejects tasks without promptRepoId (19.x) - Tests: strict mode accepts tasks with valid promptRepoId (20.x) - Tests: permissive mode behavior unchanged / non-regression (21.x) - Tests: TASK_ROUTING_STRICT is classified as fatal (22.x) - Tests: repo mode unaffected by strict routing (23.x) - Tests: end-to-end strict routing via runDiscovery pipeline (24.x) - All 87 routing tests pass
…t in discovery pipeline
… permissive routing
…est counts updated
- Add groupLanesByRepo() to partition lanes by repoId - Add mergeWaveByRepo() that runs per-repo merge loops with correct repo roots and base branches (via resolveRepoRoot/resolveBaseBranch) - In repo mode, single group passthrough preserves existing behavior - In workspace mode, each repo group merges independently; failures in one repo don't block merging in other repos - Add RepoMergeOutcome type for per-repo attribution on MergeWaveResult - Add repoId field to MergeLaneResult for post-merge branch cleanup - Update engine.ts to call mergeWaveByRepo with workspaceConfig - Update engine.ts post-merge branch cleanup to use per-lane repo root - Update resume.ts merge calls to use mergeWaveByRepo with workspaceConfig - Update resume.ts branch cleanup to use per-lane repo root - Fix hardcoded 'into develop' message text in messages.ts - Add merge-repo-scoped.test.ts (10 tests: grouping, ordering, types) - All 216 tests pass
…p, tests - Set repoId on MergeLaneResult in mergeWave() success and error paths - Fix aggregate status logic in mergeWaveByRepo() to use lane-level evidence (anyLaneSucceeded/anyLaneFailed) instead of repo-level status, correctly classifying 'all repos partial' as global partial (not failed) - Add 10 new test cases: status rollup edge cases (all-succeed, mixed, all-fail, all-partial, vacuous, error-only, error+success) and repoId propagation verification
…egate status R002 finding #1: mergeWave() can return status='failed' with failedLane=null for pre-lane setup errors (temp branch creation, worktree creation). mergeWaveByRepo() previously only checked failedLane !== null, missing these setup failures entirely. Fix: Track anyRepoFailed flag based on groupResult.status !== 'succeeded' (not just failedLane). This catches both lane-level failures AND setup failures. firstFailureReason is populated with setup error context when failedLane is null. R002 finding #2: Updated test helper computeAggregateStatus to match the real implementation (uses repoStatuses array instead of single firstFailedLane). Added 4 new test cases for setup-failure scenarios: - repo setup failure with no lanes → failed - repo setup failure + other repo success → partial - all repos setup failure → failed - repo setup failure + other repo partial → partial All 207 tests pass.
… outcomes Step 1 implementation: - Add formatRepoMergeSummary() shared helper in messages.ts - Add orchMergePartialRepoSummary template to ORCH_MESSAGES - Wire partial-summary emission in both engine.ts and resume.ts - Emit repo-divergence summary only when status=partial AND repoResults show different statuses across repos - No misleading repo-divergence text for mixed-outcome-lane partials - 8 new test assertions covering: divergence formatting, mono-repo, deterministic ordering, template usage, same-status suppression, single-group suppression, mixed-outcome-lane suppression
…y helper Step 2: Extract computeMergeFailurePolicy() pure function into messages.ts. Both engine.ts and resume.ts now use this shared helper to guarantee identical pause/abort policy decisions, failure attribution, error messages, and notifications on repo-scoped merge failures. Changes: - messages.ts: add MergeFailurePolicyResult type and computeMergeFailurePolicy() - engine.ts: replace inline merge-failure handler with shared helper call - resume.ts: replace inline merge-failure handler with shared helper call (parity fix) - tests: 7 new test sections (19-25) covering pause/abort policy, setup failures, multi-lane attribution, engine/resume parity, reason truncation, determinism
…tests, all 207 pass
…rator ID, fallback matrix, parser compat plan
…rees, branches, merge artifacts
- Create naming.ts with resolveOperatorId(), sanitizeNameComponent(), resolveRepoSlug()
- Add operator_id to OrchestratorConfig (auto-detected from OS username)
- Update TMUX sessions: {prefix}-{opId}-lane-{N} / {prefix}-{opId}-{repoId}-lane-{N}
- Update branches: task/{opId}-lane-{N}-{batchId}
- Update worktree dirs: {prefix}-{opId}-{N}
- Update merge: temp branch, workspace dir, session names, sidecar files
- Operator-scoped listWorktrees() with legacy fallback for opId='op'
- Propagate opId through waves.ts, worktree.ts, merge.ts, engine.ts, resume.ts
- Update tests for new naming patterns (207/207 passing)
…ntly, all tests passing
- Created naming-collision.test.ts with 48 tests in 4 categories: 2a: Collision test matrix (multi-operator, multi-repo, concurrent batches) 2b: Ownership-safe consumer validation (parseOrchSessionNames, sidecar cleanup) 2c: Human-readability validation (length budgets, token order, sort order) 2d: Sanitization edge cases (case folding, truncation collision documentation) - Documented prefix-only cleanup/abort as intended team behavior - Documented sanitization collision risks (case folding, special char normalization, truncation) - All 255 tests passing
Step 2: Validate collision resistance - Collision matrix: operator × repo × batch × lane uniqueness for all artifact types (TMUX sessions, worktree paths, branches, merge temp branches, merge sidecars, merge sessions, merge workspace dirs) - Shared-environment interference: parseOrchSessionNames prefix filtering, listWorktrees opId scoping, sidecar filename uniqueness, abort prefix-scoped kill (documented as intended team behavior) - Human-readability: length bounds (TMUX ≤64, branches ≤100), token order consistency, safe character validation, provenance parseability, naming contract examples verification - Naming utilities: sanitizeNameComponent, resolveOperatorId, resolveRepoSlug edge cases All 290 tests passing (207 existing + 83 new).
…collectRepoRoots helper + mixed-repo tests Step 0: Implement repo-aware reconciliation - Verified that resume.ts already has repo-aware patterns for all 4 critical areas: reconnect polling, re-execute spawning, inter-wave worktree reset, and terminal worktree cleanup (from prior TP-005/TP-006 work) - Added collectRepoRoots() helper function for collecting unique repo roots from persisted lane records (usable for test and future refactoring) - Added 10 new tests for mixed-repo reconciliation scenarios: - Workspace v2: one repo lane alive + another dead - Workspace v2: .DONE in one repo + dead session in another - v1 state (no repo fields) reconciles correctly - Worktree exists vs missing split across repos - resolveRepoRoot integration (v2 vs v1/undefined) - collectRepoRoots for workspace and repo modes - computeResumePoint with mixed cross-repo outcomes - Updated PersistedBatchStateForTest interface with mode/baseBranch fields - All 290 tests passing across 12 test files
- Fix reconnect polling to use resolveRepoRoot() per-lane - Fix re-execute spawning to use resolveRepoRoot() per-lane - Fix inter-wave worktree reset to iterate unique repo roots - Fix terminal worktree cleanup to iterate unique repo roots - Add 8 mixed-repo reconciliation tests (section 8.1) - v1 state files resume identically (undefined repoId → default root)
…d/skipped determinism - Add 'pending' reconciliation action for never-started future-wave tasks (pending + no session) to prevent incorrect mark-failed classification - Add 'skipped' to wave-skip condition in computeResumePoint (pre-existing gap) - Fix blocked task counter double-counting with persistedBlockedTaskIds tracking - Separate mark-complete from skip case in categorization for clarity - Add 8 new test cases covering pending-vs-failed, skipped wave-skip, all-failed wave, counter stability, cross-repo blocked propagation, v1 fallback parity - All 290 tests passing across 12 test files
…po-aware semantics
…ng, blocked counter, repo attribution Step 2 implementation: - Fix re-exec merge indexing: use sentinel waveIndex -1, clamp persistence normalization with Math.max(0, ...) to prevent negative indices - Fix blocked counter: count persisted-blocked tasks in unvisited waves at resume init (tasks blocked before their wave was entered were never counted) - Fix repo attribution carry-forward: reconstructAllocatedLanes now accepts persistedTasks parameter to inject repoId/resolvedRepoId/taskFolder from prior state onto reconstructed AllocatedTask stubs - Replace inline per-repo root collection loops with collectRepoRoots() helper - Add 7 new tests covering checkpoint round-trip, blocked counter semantics, re-exec merge persistence, and mixed-repo metadata preservation - All 290 tests passing across 12 test files
…ollectAllRepoRoots helper, 740 assertions passing
…passthrough
Step 0: Extend dashboard data model — complete.
- Add batch.mode field ('repo'|'workspace') to buildDashboardState()
in dashboard/server.cjs so the frontend knows the workspace mode.
- Verified that lane repoId, task repoId/resolvedRepoId, and merge
repoResults already flow through from persisted state (TP-006).
- All 290 tests passing, CLI smoke check OK.
- Additive-only changes — backward compatible.
…d repo-aware observability - Add PersistedRepoMergeOutcome type for compact per-repo merge outcomes - Add optional repoResults field to PersistedMergeResult interface - Serialize MergeWaveResult.repoResults in serializeBatchState() - Add validation for repoResults in validatePersistedState() - Additive-only: absent/undefined in repo mode, backward compatible
…and merge grouping - Add repo filter dropdown to header (index.html), hidden by default - Add repo badge styles and merge sub-row styles (style.css) - Implement buildRepoSet() to derive repos from lanes/tasks/mergeResults - Implement updateRepoFilter() with disappearing-repo reset to 'All' - Add repo badges to lane headers and task rows in renderLanesTasks() - Add per-repo sub-rows in renderMergeAgents() for workspace mode - Filter lanes/tasks/merge panels consistently by selected repo - Gate all repo UI by mode=workspace AND 2+ distinct repos - Summary bar and footer remain global (unfiltered) - No changes to conversation/STATUS.md viewer panels
- Create polyrepo-builder.ts: runtime fixture with non-git workspace root, 3 git repos (docs, api, frontend), shared task root, 3 task areas - Define 6-task matrix with cross-repo deps spanning 3 waves: Wave 1: SH-001, AP-001, UI-001 (independent) Wave 2: AP-002, UI-002 (same-repo + cross-repo deps) Wave 3: SH-002 (cross-repo deps on wave 2) - Add batch-state-v2-polyrepo.json static fixture for resume tests - Add polyrepo-fixture.test.ts with 32 acceptance tests covering: topology, workspace config, discovery/routing, wave shape, static fixture validation, and ParsedTask builder helpers - All 322 tests pass (including 32 new)
47 tests covering: - /task routing with polyrepo discovery (4 tests) - /orch-plan wave computation and lane allocation (7 tests) - Serialization of repo-aware persisted state (4 tests) - Per-repo merge outcomes and partial failures (4 tests) - Resume reconciliation and resume-point for workspace mode (10 tests) - Collision-safe naming across repos (8 tests) - Repo-aware state validation and v1→v2 upconversion (10 tests) All 369 suite tests pass.
…pass, 369/369 suite
… docs - Create monorepo-compat-regression.test.ts with 34 tests guarding: - v1→v2 persistence upconversion (mode=repo, no repo fields) - Repo-mode discovery (no routing, no resolvedRepoId) - Repo-mode naming (no repoId segments in lane IDs/sessions) - Repo-mode merge grouping (single group for undefined-repoId lanes) - Repo-mode resume (mode-agnostic eligibility, proper reconstruction) - Repo-mode serialization round-trip (serialize → validate → no repo fields) - freshOrchBatchState defaults regression - Update docs/maintainers/testing.md with: - Polyrepo fixture usage guide and when to use polyrepo vs monorepo tests - Fixture limitations (temp FS, no real git history, fixed topology) - Updated key files listing All 403 tests pass across 15 test files.
…mitation to testing.md
HenryLach
force-pushed
the
feat/polyrepo-support
branch
from
March 16, 2026 01:17
2751d48 to
e7954b0
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds workspace mode for multi-repo orchestration. Tasks can target specific repositories, lanes are allocated per-repo, merges run per-repo, and resume/abort operate across repo boundaries.
What's included (TP-001 through TP-012)
Core infrastructure:
workspace.ts)discovery.ts)routing.strict: true)Execution:
waves.ts)naming.ts)Merge:
merge.ts:mergeWaveByRepo)messages.ts:computeMergeFailurePolicy)Persistence & Resume:
persistence.ts)resume.ts)Observability:
Quality:
Stats
Validation
cd extensions && npx vitest run— 290/290 passingnode bin/taskplane.mjs help+doctor— clean